{
 "metadata": {
  "name": "",
  "signature": "sha256:b811f403e4a6b0d55531ad2eef03d8b5450b57887ba28f5a5e9a81658e367073"
 },
 "nbformat": 3,
 "nbformat_minor": 0,
 "worksheets": [
  {
   "cells": [
    {
     "cell_type": "heading",
     "level": 1,
     "metadata": {},
     "source": [
      "Sudoku recognizer"
     ]
    },
    {
     "cell_type": "heading",
     "level": 4,
     "metadata": {},
     "source": [
      "*Alejandro Hern\u00e1ndez Cordero (GitHub ID: [ahcorde](https://github.com/ahcorde)) *"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "This notebook details how to recognize a [sudoku puzzle](http://en.wikipedia.org/wiki/Sudoku) from a picture. We'll make use of simple [image processing](http://en.wikipedia.org/wiki/Image_processing) algorithms \n",
      "(edge detection, thresholds... ) and [character recognition](http://en.wikipedia.org/wiki/Optical_character_recognition) using the [K-Nearest Neighbors](http://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm) (KNN) algorithm. It is a very simple \n",
      "but effective algorithm for solving multi-class classification problems. This puzzle matrix is a 9x9 array of known numbers 1-9 or 0s where the number is unknown\n",
      "\n",
      "This ipython notebook is divided into two parts. The first part is related to computer vision and the second part is related to machine learning. To complete this task it is necessary to use a computer vision library, in this case we are going to use [OpenCV library](http://opencv.org/).\n"
     ]
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "Introduction"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "The first thing we need to do is to identify the puzzle. We have some challenges:  The lines are not perfect, the black grid lines have similar color as a lot of elements in the image or the small squares are difficult to extract.\n",
      "\n",
      "To simplify the problem we assumed: **the puzzle is the biggest square in the image and the puzzle will be orientated reasonably correctly.**\n",
      "\n",
      "In the next picture you can see a sudoku puzzle in a newspaper. To load the image it's used [imread](http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html?highlight=imread#cv2.imread)."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "%matplotlib inline\n",
      "import pylab as pl\n",
      "#import Opencv library\n",
      "try:\n",
      "    import cv2\n",
      "except ImportError:\n",
      "    print \"You must have OpenCV installed\"\n",
      "    \n",
      "#load image\n",
      "image_sudoku_original = cv2.imread('../../../data/ocr/sudoku.jpg')\n",
      "#Show Images\n",
      "_=pl.imshow(image_sudoku_original) \n",
      "_=pl.axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "Step 1: Segmenting the Sudoku"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Once we have read the image we have to detect the lines. For this task, It used an [adaptativeThreshold](http://docs.opencv.org/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold) to extract the edge of the image (for each pixel in the image take the average value of the surrounding area). This function accepts a gray scale image, with the function [cvtColor](http://docs.opencv.org/modules/imgproc/doc/miscellaneous_transformations.html#cvtcolor) we change the color space (from RGB to gray scale). In this picture it's possible to see letters, lines or numbers. Light pixels are the paper and dark pixels are the ink.\n",
      "\n",
      "The adaptativeThreshold function takes as first argument the input image, the second argument returns the binary image, the third argument is the non-zero value assigned to the pixels for which the condition is satisfied. The fourth is the adaptive thresholding algorithm, the fifth argument is the threshold type, the next argument is size of the pixel neighborhood and finally the last argument is a constant that is subtracted from the mean or weight mean."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "#gray image\n",
      "image_sudoku_gray = cv2.cvtColor(image_sudoku_original,cv2.COLOR_BGR2GRAY)\n",
      "#adaptive threshold\n",
      "thresh = cv2.adaptiveThreshold(image_sudoku_gray,255,1,1,11,15)\n",
      "\n",
      "#show image\n",
      "_=pl.imshow(thresh, cmap=pl.gray())\n",
      "_=pl.axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "We have to find the connected contours in the image (using the function [findContours](http://docs.opencv.org/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?highlight=findcontours#cv2.findContours)). These function returns a vector with the corners of the contours. We'll find the sukodu in this contours.\n",
      "\n",
      "The assumption is that the sudoku puzzle has **4 sides** and it's **convex**. Checking the number of the contour is equal to **four** and using the funcion of OpenCV [isContourConvex](http://docs.opencv.org/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?highlight=iscontourconvex#cv2.isContourConvex) to check if the **square is convex**. We obtain the possible candidates.\n",
      "\n",
      "We need to filter the candidates using the assumption: the sudoku puzzle is the biggest square in the image. We calculate the area of the possible contours ([contourArea](http://docs.opencv.org/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?highlight=iscontourconvex#contourarea)). The biggest square is the sudoku puzzle.\n"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "#find the countours \n",
      "contours0,hierarchy = cv2.findContours( thresh,\n",
      "                                        cv2.RETR_LIST,\n",
      "                                        cv2.CHAIN_APPROX_SIMPLE)\n",
      "#size of the image (height, width)\n",
      "h, w = image_sudoku_original.shape[:2]\n",
      "\n",
      "#copy the original image to show the posible candidate\n",
      "image_sudoku_candidates = image_sudoku_original.copy()\n",
      "\n",
      "#biggest rectangle\n",
      "size_rectangle_max = 0; \n",
      "for i in range(len(contours0)):\n",
      "    #aproximate countours to polygons\n",
      "    approximation = cv2.approxPolyDP(contours0[i], 4, True)\n",
      "        \n",
      "    #has the polygon 4 sides?\n",
      "    if(not (len (approximation)==4)):\n",
      "        continue;\n",
      "    #is the polygon convex ?\n",
      "    if(not cv2.isContourConvex(approximation) ):\n",
      "        continue; \n",
      "    #area of the polygon\n",
      "    size_rectangle = cv2.contourArea(approximation)\n",
      "    #store the biggest\n",
      "    if size_rectangle> size_rectangle_max:\n",
      "        size_rectangle_max = size_rectangle \n",
      "        big_rectangle = approximation\n"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "In the image below it's possible to see the 4 sides of the sudoku puzzle in red"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "#show the best candidate\n",
      "approximation = big_rectangle\n",
      "for i in range(len(approximation)):\n",
      "    cv2.line(image_sudoku_candidates,\n",
      "             (big_rectangle[(i%4)][0][0], big_rectangle[(i%4)][0][1]), \n",
      "             (big_rectangle[((i+1)%4)][0][0], big_rectangle[((i+1)%4)][0][1]),\n",
      "             (255, 0, 0), 2)\n",
      "#show image\n",
      "_=pl.imshow(image_sudoku_candidates, cmap=pl.gray()) \n",
      "_=pl.axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Now we have the sudoku puzzle segmented. We have got the corner points of the puzzle. It's currently not really usable for much. The sudoku puzzle is a bit distorted. It's necessary to correct the skewed perspective of the image. We need a way to mapping from the puzzle in the original picture back into a square. Where each corner of the sudoku puzzle corresponds to a corner on the a new image. \n",
      "\n",
      "We use a transformation that will map one arbitrary 2D quadrilateral into another. We can use a perspective transformation:\n"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "$$X = \\frac{ax + by + c}{gx + hy +1}$$\n",
      "$$Y = \\frac{dx + ey + f}{gx + hy +1}$$"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "This perspective transformation maps a point $ (x, y) $ in one quadrilateral into a new  $ (X, Y) $ in another quadrilateral. These two equations contain 8 unknowns, but we have 8 values. (the corners $x$ and $y$ coordinates of the puzzle). Solving these equations gives us the $a,b,c,d,e,f,g,h$ which provide us with a mapping to get our puzzle out nice and straight. \n",
      "\n",
      "The OpenCV function [getperspectivetransform](http://docs.opencv.org/modules/imgproc/doc/geometric_transformations.html#getperspectivetransform) resolved the perspective transformation. Calculates a perspective transform from four pairs of the corresponding points, where the first parameter are the coordinates of quadrangle vertices in the source image and the second parameter are the coordinates of the corresponding quadrangle vertices in the destination image.\n",
      "\n",
      "To applies a perspective transformation to an image we used the function [warpPerspective](http://docs.opencv.org/modules/imgproc/doc/geometric_transformations.html#warpperspective). This function transforms the source image using the specified matrix. We obtains the image below."
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "We need to sort the corner of the sudoku puzzle and then associate each point with the new image dimension."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "import numpy as np\n",
      "IMAGE_WIDHT = 16\n",
      "IMAGE_HEIGHT = 16\n",
      "SUDOKU_SIZE= 9\n",
      "N_MIN_ACTVE_PIXELS = 10\n",
      "\n",
      "#sort the corners to remap the image\n",
      "def getOuterPoints(rcCorners):\n",
      "    ar = [];\n",
      "    ar.append(rcCorners[0,0,:]);\n",
      "    ar.append(rcCorners[1,0,:]);\n",
      "    ar.append(rcCorners[2,0,:]);\n",
      "    ar.append(rcCorners[3,0,:]);\n",
      "    \n",
      "    x_sum = sum(rcCorners[x, 0, 0] for x in range(len(rcCorners)) ) / len(rcCorners)\n",
      "    y_sum = sum(rcCorners[x, 0, 1] for x in range(len(rcCorners)) ) / len(rcCorners)\n",
      "    \n",
      "    def algo(v):\n",
      "        return (math.atan2(v[0] - x_sum, v[1] - y_sum)\n",
      "                + 2 * math.pi) % 2*math.pi\n",
      "        ar.sort(key=algo)\n",
      "    return (  ar[3], ar[0], ar[1], ar[2])\n"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "The dataset images have **16x16 pixels**. The size of the new image will be **144x144**, because we have to divide each row and col by 9 and we have to return the same size of the images."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "#point to remap\n",
      "points1 = np.array([\n",
      "                    np.array([0.0,0.0] ,np.float32) + np.array([144,0], np.float32),\n",
      "                    np.array([0.0,0.0] ,np.float32),\n",
      "                    np.array([0.0,0.0] ,np.float32) + np.array([0.0,144], np.float32),\n",
      "                    np.array([0.0,0.0] ,np.float32) + np.array([144,144], np.float32),\n",
      "                    ],np.float32)    \n",
      "outerPoints = getOuterPoints(approximation)\n",
      "points2 = np.array(outerPoints,np.float32)\n",
      "\n",
      "#Transformation matrix\n",
      "pers = cv2.getPerspectiveTransform(points2,  points1 );\n",
      "\n",
      "#remap the image\n",
      "warp = cv2.warpPerspective(image_sudoku_original, pers, (SUDOKU_SIZE*IMAGE_HEIGHT, SUDOKU_SIZE*IMAGE_WIDHT));\n",
      "warp_gray = cv2.cvtColor(warp, cv2.COLOR_BGR2GRAY)\n",
      "\n",
      "#show image\n",
      "_=pl.imshow(warp_gray, cmap=pl.gray())\n",
      "_=pl.axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "we've now got an undistorted square Sudoku puzzle"
     ]
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "Step 2: Segmenting the numbers"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "After remapping, we can divide the puzzle into a 9\u00d79 grid. Each cell in the grid will correspond (approximately) to a cell on the puzzle. The correspondence might not be perfect, but it should be good enough.\n",
      "\n",
      "Now, we know the size of the bounding box. We also know the size of the image. We can easily center the image. We create a new image that\u2019s the same size as the original. \n",
      "\n",
      "Sending images directly to k-Nearest Neighbors is not a good idea. A little bit of work on the image can increase accuracy. Here, we\u2019ll center the actual digit in the image. The dataset has been made in such a way, all digits are centered by their bounding box."
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "To remove the noise around the number (lines in general) we delete all the pixels outside the center of the image from a radius."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "def extract_number(x, y):\n",
      "    #square -> position x-y\n",
      "    im_number = warp_gray[x*IMAGE_HEIGHT:(x+1)*IMAGE_HEIGHT][:, y*IMAGE_WIDHT:(y+1)*IMAGE_WIDHT]\n",
      "\n",
      "    #threshold\n",
      "    im_number_thresh = cv2.adaptiveThreshold(im_number,255,1,1,15,9)\n",
      "    #delete active pixel in a radius (from center) \n",
      "    for i in range(im_number.shape[0]):\n",
      "        for j in range(im_number.shape[1]):\n",
      "            dist_center = math.sqrt( (IMAGE_WIDHT/2 - i)**2  + (IMAGE_HEIGHT/2 - j)**2);\n",
      "            if dist_center > 6:\n",
      "                im_number_thresh[i,j] = 0;\n",
      "\n",
      "    n_active_pixels = cv2.countNonZero(im_number_thresh)\n",
      "    return [im_number, im_number_thresh, n_active_pixels]"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Sometimes after remove the noise of the images can appear other particles close to the number. Because of this, we find the biggest bounding box in the small squared. We make the bounding box a little more bigger to improve the matching with the dataset."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "def find_biggest_bounding_box(im_number_thresh):\n",
      "    contour,hierarchy = cv2.findContours(im_number_thresh.copy(),\n",
      "                                         cv2.RETR_CCOMP,\n",
      "                                         cv2.CHAIN_APPROX_SIMPLE)\n",
      "\n",
      "    biggest_bound_rect = [];\n",
      "    bound_rect_max_size = 0;\n",
      "    for i in range(len(contour)):\n",
      "         bound_rect = cv2.boundingRect(contour[i])\n",
      "         size_bound_rect = bound_rect[2]*bound_rect[3]\n",
      "         if  size_bound_rect  > bound_rect_max_size:\n",
      "             bound_rect_max_size = size_bound_rect\n",
      "             biggest_bound_rect = bound_rect\n",
      "    #bounding box a little more bigger\n",
      "    x_b, y_b, w, h = biggest_bound_rect;\n",
      "    x_b= x_b-1;\n",
      "    y_b= y_b-1;\n",
      "    w = w+2;\n",
      "    h = h+2; \n",
      "                \n",
      "    return [x_b, y_b, w, h]"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Now we have to separate all the numbers from the grid. First we extract the numbers. If we have more active pixels than a threshold, we find the biggest bounding box in the image if the size of this bounding box is bigger than 0,  we store this number in a matrix. In another case we store a matrix of zeros."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "import math\n",
      "import numpy as np\n",
      "\n",
      "#sudoku representation\n",
      "sudoku = np.zeros(shape=(9*9,IMAGE_WIDHT*IMAGE_HEIGHT))\n",
      "\n",
      "def Recognize_number( x, y):\n",
      "    \"\"\"\n",
      "    Recognize the number in the rectangle\n",
      "    \"\"\"    \n",
      "    #extract the number (small squares)\n",
      "    [im_number, im_number_thresh, n_active_pixels] = extract_number(x, y)\n",
      "\n",
      "    if n_active_pixels> N_MIN_ACTVE_PIXELS:\n",
      "        [x_b, y_b, w, h] = find_biggest_bounding_box(im_number_thresh)\n",
      "\n",
      "        im_t = cv2.adaptiveThreshold(im_number,255,1,1,15,9);\n",
      "        number = im_t[y_b:y_b+h, x_b:x_b+w]\n",
      "\n",
      "        if number.shape[0]*number.shape[1]>0:\n",
      "            number = cv2.resize(number, (IMAGE_WIDHT, IMAGE_HEIGHT), interpolation=cv2.INTER_LINEAR)\n",
      "            ret,number2 = cv2.threshold(number, 127, 255, 0)\n",
      "            number = number2.reshape(1, IMAGE_WIDHT*IMAGE_HEIGHT)\n",
      "            sudoku[x*9+y, :] = number;\n",
      "            return 1\n",
      "\n",
      "        else:\n",
      "            sudoku[x*9+y, :] = np.zeros(shape=(1, IMAGE_WIDHT*IMAGE_HEIGHT));\n",
      "            return 0\n"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Here you can see the segmented numbers in a grid"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "index_subplot=0\n",
      "n_numbers=0\n",
      "indexes_numbers = []\n",
      "for i in range(SUDOKU_SIZE):\n",
      "    for j in range(SUDOKU_SIZE):\n",
      "        if Recognize_number(i, j)==1:\n",
      "            if (n_numbers%5)==0:\n",
      "                index_subplot=index_subplot+1\n",
      "            indexes_numbers.insert(n_numbers, i*9+j)\n",
      "            n_numbers=n_numbers+1\n",
      "\n",
      "#create subfigures\n",
      "f,axarr= pl.subplots(index_subplot,5)\n",
      "\n",
      "width = 0;\n",
      "for i in range(len(indexes_numbers)):\n",
      "    ind = indexes_numbers[i]\n",
      "    if (i%5)==0 and i!=0:\n",
      "        width=width+1\n",
      "    axarr[i%5, width].imshow(cv2.resize(sudoku[ind, :].reshape(IMAGE_WIDHT,IMAGE_HEIGHT), (IMAGE_WIDHT*5,IMAGE_HEIGHT*5)), cmap=pl.gray())\n",
      "    axarr[i%5, width].axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "Step 3: Recognizing digits (Machine learning)"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "In machine learning, classification is the problem of identifying to which of a set of categories a new observation belongs. Classification is an example of the more general problem of pattern recognition.  Which is the assignment of some sort of output value to a given input value.  The k-Nearest Neighbors algorithm (or KNN) is a non-parametric method used for classification and regression.\n",
      "\n",
      "In [SHOGUN](http://www.shogun-toolbox.org/), you can use [CKNN](http://www.shogun-toolbox.org/doc/en/latest/classshogun_1_1CKNN.html) to perform  k-Nearest Neighbors algorithm, it's a non-parametric method used for classification and regression. To construct a KNN machine, you must choose the hyper-parameter K and a distance function. \n",
      "Usually, we simply use the standard [CEuclideanDistance](http://www.shogun-toolbox.org/doc/en/3.0.0/classshogun_1_1CEuclideanDistance.html), but in general, any subclass of [CDistance](http://www.shogun-toolbox.org/doc/en/3.0.0/classshogun_1_1CDistance.html) could be used.\n",
      "\n",
      "We need to load the training data. but the dataset values are between -1 and 1. We need to normalize the data between 0-255."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "from modshogun import MulticlassLabels,RealFeatures\n",
      "from modshogun import KNN, EuclideanDistance\n",
      "from scipy.io import loadmat\n",
      "\n",
      "from numpy    import random\n",
      "\n",
      "#load data\n",
      "mat  = loadmat('../../../data/multiclass/usps.mat')\n",
      "Xall = mat['data']\n",
      "#normalize between 0-255\n",
      "normalize = np.zeros((Xall.shape[1], Xall.shape[0],1), np.uint8)\n",
      "normalize= cv2.normalize(Xall, normalize, 0, 255, cv2.NORM_MINMAX, cv2.CV_8UC1);\n",
      "Xall = np.asmatrix(normalize, np.float64)\n",
      "\n",
      "#load labels\n",
      "Yall = np.array(mat['label'].squeeze(), dtype=np.double)\n",
      "Yall = Yall - 1\n",
      "\n",
      "#random position to train knn\n",
      "random.seed(0)\n",
      "\n",
      "subset = random.permutation(len(Yall))\n",
      "test_images = Xall[:, subset[:5000]]\n",
      "test_labels = Yall[subset[:5000]]"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "We initialize the knn, set up the features and configure the kind of distance that we want to use (in this case Euclidean distance)."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "k=10\n",
      "\n",
      "labels_numbers = MulticlassLabels(test_labels)\n",
      "feats  = RealFeatures(test_images)\n",
      "dist = EuclideanDistance()\n",
      "knn = KNN(k, dist, labels_numbers)\n",
      "_=knn.train(feats)"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Now we have to take all the segmented numbers and returns what digit it is."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "sudoku_puzzle = np.zeros(shape=(9,9))\n",
      "\n",
      "#Predict the number\n",
      "for i in range(SUDOKU_SIZE):\n",
      "    for j in range(SUDOKU_SIZE):         \n",
      "        #has the image some active pixel?    \n",
      "        n_active_pixels = cv2.countNonZero(np.asmatrix(sudoku[i*9+j, :]))\n",
      "        if n_active_pixels>20 :\n",
      "            feats_test  = RealFeatures( np.asmatrix(sudoku[i*9+j, :], np.float64).T)\n",
      "            sudoku_puzzle[i,j] = knn.apply_multiclass(feats_test)[0]\n",
      "        else:\n",
      "            sudoku_puzzle[i,j] = 0\n",
      "            \n",
      "print sudoku_puzzle\n",
      "#show image\n",
      "_=pl.imshow(image_sudoku_original)\n",
      "_=pl.axis(\"off\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": []
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "The recognition isn't very accurate because the training data is for handwritten text.\n",
      "\n",
      "The accuracy of the process could be improved. In this example we have used a holistic method, all the image is introduced in the classifier. But adding new features to the classifier, for example the number of holes, number of straight lines or length of the lines, could improve the accuracy of the classifier."
     ]
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "References"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "- [Shogun documentation](http://www.shogun-toolbox.org/doc/en/latest/)\n",
      "- [OpenCV documentation](http://docs.opencv.org/)"
     ]
    }
   ],
   "metadata": {}
  }
 ]
}
